Skip to content

test: fix Bun-canary failures (quoted CSS url(), cache marker polling race) - #375

Merged
khromov merged 3 commits into
mainfrom
fix/bun-canary-css-assets
Oct 30, 2026
Merged

khromov merged 3 commits into
mainfrom
fix/bun-canary-css-assets

Conversation

@khromov

@khromov khromov commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Fixes the two Bun-canary test failures in test.yml. Both are test-only changes; no framework code changes.

1. cssFontImports.test.ts — consistent canary failure

Root cause: an intentional Bun output change, not a Bun bug or a mochi bug. oven-sh/bun#47025 ("css: consolidate the open CSS parser fixes", commit 794200868, between 1.4.4 38afee6c8 and canary da867c60b) now prints:

  • "The path of a copied asset spliced into url() is always quoted, so a file name with a space or a parenthesis cannot end the url()" (#41557)
  • "@font-face format() keywords as strings" (#37979)

The test extracted the asset name with /url\(\.\/([^)"']+\.png)\)/, which only matches the unquoted form. On canary, every sheet returned undefined, so names was {undefined}. Its size of 1 passed, and toMatch threw Received value must be a string: undefined.

Standalone repro (no mochi): Bun.build({ entrypoints: ['a.css'], minify: true }) on .a{background:url("./shared.png")} with a 200 KB png:

1.4.4 38afee6c8  .a{background:url(./shared-fpg4xa8b.png)}…format(woff2)
1.4.4 da867c60b  .a{background:url("./shared-fpg4xa8b.png")}…format("woff2")

Production code is unaffected, because cssFontAssets.ts reads url() and format() through the css-tree AST (parseCss in cssAst.ts), which decodes quoting.

Fix (after review): the tests now read bundled CSS the same way. cssFontImports.test.ts gains three test-local helpers:

  • parsed() wraps parseCss and throws a clear error on a null result.
  • urlValues() returns the decoded url() values.
  • relativePng() returns the first non-data: .png url containing a given stem.

Converted from regex extraction to the AST, each kept at least as strict:

  • :98 checks the served woff2 URL and its format() on the same src entry, where the old regex only required them to be adjacent text.
  • :105 checks that no font source has format woff, guarded against an empty source list.
  • :302 checks that no relative (non-/_mochi/fonts/) .woff2 url is left.
  • :388, :401, :407, :460, :487 read the relative image urls.
  • :550 is the canonical-name test. Each sheet must contribute exactly one ./-relative .png url, otherwise it adds undefined to the set. There must be exactly one name, and it must match ^shared-[a-z0-9]{8}\.png$.

Left as text checks on purpose:

  • toContain/not.toContain presence checks on URLs and data: payloads (:108, :113, :192, :202, :233, :299-301, :312-313, :323-324, :400, :459, :543). They don't extract a value, they don't depend on quoting, and most match mochi's own substituted text.
  • cssImports.test.ts:57-59 asserts the raw minified text.
  • cssImports.test.ts:123-124 asserts the serialized quoting of format('woff2-variations') itself. The AST's format lowercases both the string and the identifier form, so it cannot tell them apart.
  • All of cssFontAssets.test.ts checks mochi's own substituteFontUrls/classifyFontAssets output on hand-written input, with no Bun involved.

No production change: parseCss already exposes everything the tests needed.

Mutation checks on the converted file, with production code broken temporarily and then restored:

Mutation Result
Keep the per-build suffix (canonical = assetPath) 10 fail, including the canonical-name test
Stop dropping legacy woff drops the legacy woff source entirely fails
Make substituteFontUrls write a wrong URL rewrites the large woff2… fails
before after
stable 1.4.4 (38afee6c8) 22/22 22/22
canary 1.4.4-canary.1 (da867c60b) 21 pass, 1 fail 22/22

2. cache-storage-file.test.ts — probable flake

I could not reproduce it. I ran it 0/320 failures: 100 runs each on stable and canary at idle, and 60 runs each with 24 busy loops on 12 cores plus sync dd I/O. Reading the test does show a real timing assumption, though. waitForMarker polled for at most 250 × (getItem + 2 ms), which is about 530 ms of wall time. After release(), A must do a durable setItem (fsync + rename) and then remove the marker. Locally that takes about 2 ms. This file's own LEASE comment records CI runs of 400–1300 ms per test.

Injecting an 800 ms delay into A's setItem reproduces the exact CI error and stack: marker for "k" never became absent from waitForMarker, called from the line after release().

The fix replaces the fixed-budget polling with deterministic synchronization:

  • Marker present: wait until fn is entered (run() awaits writeMarker before calling fn), then assert that the marker exists.
  • Marker absent: await cacheA.whenIdle() (it settles only after the run's finally { await removeMarker }), or await second in the superseded-run test. Then assert once that the marker is null.

These checks are at least as strict as before: the marker must be gone the moment the run settles, with no grace window. With the 800 ms injection, the new test passes. With removeMarker mutated out, the cross-process tests still fail. waitForMarker is deleted because nothing uses it now.

Verification

  • bun run checks (stable 1.4.4): pass. mochi-framework passed 204/204 test files.
  • bun run build (stable 1.4.4): pass.
  • cssFontImports.test.ts + cssImports.test.ts: 31/31 on stable and on canary.
  • After the AST conversion: cssFontImports.test.ts 22/22 on stable 1.4.4 and on canary 1.4.4-canary.1 (da867c60b). bun run checks (stable): pass.
  • cache-storage-file.test.ts: 37/37 on stable and on canary. The cross-process block also passed 40/40 on each version under CPU load.

No upstream issue is needed: the Bun change is intentional and valid CSS.

Bun canary (oven-sh/bun#47025) always quotes the copied asset path it
splices into url(), so the unquoted-only regex matched nothing and the
canonical-name assertion saw undefined.
…aits

The cross-process marker tests polled for the marker for ~0.5s, racing
the durable value write and marker removal on slow CI disks. Wait on fn
being entered (the marker is written first) and on whenIdle() (settles
after marker removal) instead, then assert the marker state directly.
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Mochi review report

Try this PR

Expand instructions
gh run download -R khromov/mochi 40983939550 -n mochi-framework-pr -D /tmp/mochi-pr && bun i /tmp/mochi-pr/mochi-framework-pr.tgz

Download manually

Dependency report

Expand report
Direct: 18
Peer:   5 (@mochi-framework/rsvelte, @mochi-framework/svelte-shaker, @tailwindcss/node, @tailwindcss/oxide, svelte)
Dev:    12
Optional (installed, folded in): 0
Total unique packages reachable from production deps (roots + transitive): 39
Total on-disk size of those packages: 12.13 MB

Toplist — direct deps ranked by total size (self + transitive):
      total       self  count  package
    4.48 MB    2.74 MB     19  svelte
    2.13 MB    1.30 MB      2  css-tree
    1.68 MB    1.68 MB      0  nodemailer
   868.6 kB   868.6 kB      0  @noble/hashes
   732.6 kB   732.6 kB      0  @css-inline/css-inline-wasm
   709.8 kB   709.8 kB      0  @noble/ciphers
   601.2 kB   601.2 kB      0  diff
   507.5 kB   356.4 kB      1  bun-boss
   151.4 kB    66.4 kB      1  magic-string
   131.3 kB   113.3 kB      1  @joint-ops/hitlimit-bun
   129.2 kB   129.2 kB      0  html-entities
   107.8 kB   107.8 kB      0  devalue
   102.2 kB    80.1 kB      1  chokidar
    62.3 kB    62.3 kB      0  ipaddr.js
    30.4 kB    30.4 kB      0  deepmerge
    28.0 kB    28.0 kB      0  negotiator
    25.8 kB    25.8 kB      0  mitt
    12.3 kB    12.3 kB      0  zimmerframe

Transitive breakdown for the heaviest deps:

  svelte (19, 1.74 MB transitive): @jridgewell/gen-mapping (91.6 kB), @jridgewell/remapping (58.0 kB), @jridgewell/resolve-uri (51.9 kB), @jridgewell/sourcemap-codec (85.0 kB), @jridgewell/trace-mapping (143.3 kB), @sveltejs/acorn-typescript (194.2 kB), @types/estree (25.5 kB), @types/trusted-types (8.4 kB), acorn (545.5 kB), aria-query (172.8 kB), axobject-query (108.3 kB), clsx (8.4 kB), devalue (107.8 kB), esm-env (3.7 kB), esrap (91.4 kB), is-reference (3.9 kB), locate-character (5.2 kB), magic-string (66.4 kB), zimmerframe (12.3 kB)

  css-tree (2, 854.6 kB transitive): mdn-data (715.2 kB), source-map-js (139.4 kB)

  bun-boss (1, 151.1 kB transitive): croner (151.1 kB)

  magic-string (1, 85.0 kB transitive): @jridgewell/sourcemap-codec (85.0 kB)

  @joint-ops/hitlimit-bun (1, 18.0 kB transitive): @joint-ops/hitlimit-types (18.0 kB)

  chokidar (1, 22.1 kB transitive): readdirp (22.1 kB)

Security & licenses

bun audit
bun audit v1.4.4 (38afee6c8)

deepmerge@4.3.1
  workspace:mochi-framework > deepmerge
  low: deepmerge mergeObject() allows prototype poisoning of returned objects (>=4.2.0 <=4.3.1) - https://github.com/advisories/GHSA-gcqx-w48m-w2h4

extract-zip@2.0.1
  @memlab/api > puppeteer-core > @puppeteer/browsers > extract-zip
  high: extract-zip unvalidated symlink path traversal (<=2.0.1) - https://github.com/advisories/GHSA-jmr9-qjv8-65gv
  high: extract-zip allows arbitrary file writes through symlink archive entries (<=2.0.1) - https://github.com/advisories/GHSA-7pqw-9j4j-h8q3

happy-dom@20.14.6
  workspace:mochi-framework > @happy-dom/global-registrator > happy-dom
  high: Happy DOM: Incomplete fix for CVE-2026-33943: unsanitized import alias leads to host code execution (>=17.0.0 <=21.0.3) - https://github.com/advisories/GHSA-834v-8h66-3h79
  high: Happy DOM: SSRF via iframe and link stylesheet when parsing untrusted HTML (<=20.14.6) - https://github.com/advisories/GHSA-c8p4-j97h-3vg8

satori@0.26.0
  workspace:mochi-video-animations > satori
  moderate: Satori-generated SVG has improper escaping (>=0.0.27 <0.33.5) - https://github.com/advisories/GHSA-wx4j-mvgx-mqwp

zod@3.25.76
  workspace:mochi-site > runed > zod
  @memlab/api > puppeteer-core > chromium-bidi > zod
  high: Zod can exhaust memory while validating oversized constrained collections (<4.6.6) - https://github.com/advisories/GHSA-mrhx-c7xc-m3w5

7 vulnerabilities (5 high, 1 moderate, 1 low)

  bun audit fix           upgrade the vulnerable packages within their ranges
  bun audit fix --latest  also cross major versions
bun pm licenses --prod
bun pm licenses v1.4.4 (38afee6c8)

Apache-2.0 (6)
├── aria-query@5.3.1
├── axobject-query@4.1.0
├── detect-libc@2.1.2
├── schema-dts@2.0.0
├── schema-dts-lib@1.0.0
└── typescript@6.0.3

BSD-2-Clause (1)
└── entities@8.0.0

BSD-3-Clause (11)
├── d3-array@2.12.1
├── d3-interpolate-path@2.3.0
├── d3-path@1.0.9
├── d3-sankey@0.12.3
├── d3-shape@1.3.7
├── d3-tile@1.0.0
├── diff@9.0.0
├── fontverter@2.0.0
├── rw@1.3.3
├── source-map-js@1.2.2
└── subset-font@2.5.0

CC0-1.0 (1)
└── mdn-data@2.27.1

ISC (33)
├── @lucide/svelte@1.28.0
├── @ungap/structured-clone@1.3.1
├── css-color-keywords@1.0.0
├── d3-array@3.2.4
├── d3-chord@3.0.1
├── d3-color@3.1.0
├── d3-contour@4.0.2
├── d3-delaunay@6.0.4
├── d3-dispatch@3.0.1
├── d3-dsv@3.0.1
├── d3-force@3.0.0
├── d3-format@3.1.2
├── d3-geo@3.1.1
├── d3-geo-voronoi@2.1.0
├── d3-hierarchy@3.1.2
├── d3-interpolate@3.0.1
├── d3-path@3.1.0
├── d3-quadtree@3.0.1
├── d3-random@3.0.1
├── d3-scale@4.0.2
├── d3-scale-chromatic@3.1.0
├── d3-shape@3.2.0
├── d3-time@3.1.0
├── d3-time-format@4.1.0
├── d3-timer@3.0.1
├── d3-tricontour@1.1.0
├── delaunator@5.1.0
├── github-slugger@2.0.0
├── graceful-fs@4.2.11
├── internmap@1.0.1
├── internmap@2.0.3
├── semver@7.8.5
└── uri-template-matcher@1.1.2

MIT (179)
├── @clack/core@1.4.3
├── @clack/prompts@1.7.0
├── @css-inline/css-inline-wasm@0.21.0
├── @dagrejs/dagre@2.0.4
├── @dagrejs/graphlib@3.0.4
├── @floating-ui/core@1.8.0
├── @floating-ui/dom@1.8.0
├── @floating-ui/utils@0.2.12
├── @joint-ops/hitlimit-bun@1.5.0
├── @joint-ops/hitlimit-types@1.5.0
├── @jridgewell/gen-mapping@0.3.13
├── @jridgewell/remapping@2.3.5
├── @jridgewell/resolve-uri@3.1.2
├── @jridgewell/sourcemap-codec@1.5.5
├── @jridgewell/trace-mapping@0.3.31
├── @layerstack/svelte-actions@1.0.1-next.18
├── @layerstack/svelte-state@0.1.0-next.23
├── @layerstack/tailwind@2.0.0-next.21
├── @layerstack/utils@2.0.0-next.18
├── @napi-rs/canvas@1.0.3
├── @napi-rs/canvas-linux-x64-gnu@1.0.3
├── @napi-rs/canvas-linux-x64-musl@1.0.3
├── @noble/ciphers@2.2.0
├── @noble/hashes@2.2.0
├── @nprapps/sidechain@1.1.5
├── @portabletext/svelte@3.0.1
├── @portabletext/toolkit@2.0.18
├── @portabletext/types@2.0.15
├── @rsvelte/compiler@0.9.4
├── @rsvelte/vite-plugin-svelte-native@0.3.3
├── @rsvelte/vite-plugin-svelte-native-linux-x64-gnu@0.3.3
├── @shuding/opentype.js@1.4.0-beta.0
├── @standard-schema/spec@1.1.0
├── @sveltejs/acorn-typescript@1.0.10
├── @tailwindcss/node@4.3.3
├── @tailwindcss/oxide@4.3.3
├── @tailwindcss/oxide-linux-x64-gnu@4.3.3
├── @tailwindcss/oxide-linux-x64-musl@4.3.3
├── @tanstack/store@0.11.1
├── @tanstack/svelte-store@0.12.1
├── @tanstack/svelte-table@9.0.0
├── @tanstack/table-core@9.0.0
├── @tmcp/adapter-valibot@0.1.6
├── @tmcp/session-manager@0.2.2
├── @tmcp/transport-http@0.8.6
├── @types/bun@1.4.4
├── @types/d3-array@3.2.2
├── @types/d3-contour@3.0.6
├── @types/estree@1.0.9
├── @types/geojson@7946.0.16
├── @types/hast@3.0.4
├── @types/mdast@4.0.4
├── @types/node@25.9.1
├── @types/trusted-types@2.0.7
├── @types/unist@2.0.11
├── @types/unist@3.0.3
├── @typescript-eslint/types@8.65.0
├── @valibot/to-json-schema@1.7.1
├── acorn@8.16.0
├── base64-js@0.0.8
├── bun-boss@0.5.1
├── bun-types@1.4.4
├── camelize@1.0.1
├── ccount@2.0.1
├── character-entities-html4@2.1.0
├── character-entities-legacy@3.0.0
├── chokidar@5.0.0
├── clsx@2.1.1
├── color-name@1.1.4
├── comma-separated-tokens@2.0.3
├── commander@7.2.0
├── commander@15.0.0
├── croner@10.0.1
├── css-background-parser@0.1.0
├── css-box-shadow@1.0.0-3
├── css-gradient-parser@0.0.17
├── css-to-react-native@3.2.0
├── css-tree@3.2.1
├── deepmerge@4.3.1
├── dequal@2.0.3
├── devalue@5.9.3
├── devlop@1.1.0
├── emoji-regex-xs@2.0.1
├── enhanced-resolve@5.24.3
├── escape-html@1.0.3
├── esm-env@1.2.2
├── esrap@2.2.13
├── fast-string-truncated-width@3.0.3
├── fast-string-width@3.0.2
├── fast-wrap-ansi@0.2.2
├── fflate@0.7.5
├── harfbuzzjs@0.10.3
├── hast-util-heading-rank@3.0.0
├── hast-util-to-html@9.0.5
├── hast-util-to-string@3.0.1
├── hast-util-whitespace@3.0.0
├── hex-rgb@4.3.0
├── html-entities@2.6.0
├── html-void-elements@3.0.0
├── iconv-lite@0.6.3
├── inline-style-parser@0.2.7
├── ipaddr.js@2.4.0
├── is-reference@3.0.3
├── jiti@2.7.0
├── json-rpc-2.0@1.7.1
├── layerchart@2.0.4
├── linebreak@1.1.0
├── locate-character@3.0.0
├── lodash@4.18.1
├── lz-string@1.5.0
├── magic-string@0.30.21
├── magic-string@1.0.0
├── magic-string@1.1.0
├── mdast-util-to-hast@13.2.1
├── mdsvex@0.12.8
├── memoize@10.2.0
├── micromark-util-character@2.1.1
├── micromark-util-encode@2.0.1
├── micromark-util-sanitize-uri@2.0.1
├── micromark-util-symbol@2.0.1
├── micromark-util-types@2.0.2
├── mimic-function@5.0.1
├── mitt@3.0.1
├── mode-watcher@1.1.0
├── negotiator@1.0.0
├── p-limit@3.1.0
├── pako@0.2.9
├── parse-css-color@0.2.1
├── picomatch@4.0.5
├── postcss-value-parser@4.2.0
├── prism-svelte@0.4.7
├── prismjs@1.30.0
├── property-information@7.1.0
├── readdirp@5.0.0
├── rehype-slug@6.0.0
├── runed@0.37.1
├── safer-buffer@2.1.2
├── sisteransi@1.0.5
├── space-separated-tokens@2.0.2
├── sqids@0.3.0
├── string.prototype.codepointat@0.2.1
├── stringify-entities@4.0.4
├── style-to-object@1.0.14
├── svelte@5.56.8
├── svelte-french-toast@2.0.0-alpha.0
├── svelte-meta-tags@5.0.2
├── svelte-shaker@0.18.1
├── svelte-shaker-engine-scan-native@0.3.0
├── svelte-writable-derived@3.1.1
├── tailwind-merge@3.6.0
├── tailwindcss@4.3.3
├── tapable@2.3.3
├── tiny-inflate@1.0.3
├── tmcp@1.19.4
├── trim-lines@3.0.1
├── undici-types@7.24.6
├── unicode-trie@2.0.0
├── unist-util-is@4.1.0
├── unist-util-is@6.0.1
├── unist-util-position@5.0.0
├── unist-util-stringify-position@2.0.3
├── unist-util-stringify-position@4.0.0
├── unist-util-visit@2.0.3
├── unist-util-visit@5.1.0
├── unist-util-visit-parents@3.1.1
├── unist-util-visit-parents@6.0.2
├── uqr@0.1.3
├── valibot@1.4.2
├── varlock@1.16.0
├── vfile@6.0.3
├── vfile-message@2.0.4
├── vfile-message@4.0.3
├── wawoff2@2.0.1
├── woff2sfnt-sfnt2woff@1.0.0
├── yocto-queue@0.1.0
├── yoga-layout@3.2.1
├── zimmerframe@1.1.4
├── zod@3.25.76
└── zwitch@2.0.4

(MIT AND Zlib) (1)
└── pako@1.0.11

MIT-0 (1)
└── nodemailer@10.3.1

MPL-2.0 (7)
├── @resvg/resvg-js@2.6.2
├── @resvg/resvg-js-linux-x64-gnu@2.6.2
├── @resvg/resvg-js-linux-x64-musl@2.6.2
├── lightningcss@1.32.0
├── lightningcss-linux-x64-gnu@1.32.0
├── lightningcss-linux-x64-musl@1.32.0
└── satori@0.26.0

OFL-1.1 (5)
├── @fontsource-variable/fraunces@5.3.0
├── @fontsource-variable/public-sans@5.3.0
├── @fontsource/jetbrains-mono@5.3.0
├── @fontsource/lobster-two@5.3.0
└── @fontsource/public-sans@5.3.0

Python-2.0 (1)
└── argparse@2.0.1

Unlicense (1)
└── robust-predicates@3.0.3

Unknown (14)
├── @twinkleplop/bash@0.1.0
├── @twinkleplop/core@0.1.0
├── @twinkleplop/css@0.1.0
├── @twinkleplop/html@0.1.0
├── @twinkleplop/javascript@0.1.0
├── @twinkleplop/json@0.1.0
├── @twinkleplop/svelte@0.1.0
├── @twinkleplop/toml@0.1.0
├── @twinkleplop/typescript@0.1.0
├── @twinkleplop/yaml@0.1.0
├── runed@0.23.4
├── runed@0.25.0
├── svelte-toolbelt@0.7.1
└── svelte-youtube-lite@1.3.0

261 packages across 13 licenses (checked 261 packages in bun.lock) [8.00ms]

Lines of code

packages/mochi
Category main PR Δ
src/**/*.test.ts 25756 25783 +27
Total 54987 55014 +27

Unchanged: src/Mochi.ts (2116), src/{events,log,logger}.ts (469), src/extensions.ts (318), src/image/** (1797), src/middleware/** (72), src/{types.ts,*.d.ts} (977), src/web-components/** (756), src/debug-bar/** (3436), src/templates/** (1720), Other (17570).

packages/docs
Category main PR Δ
Total 6710 6710 0

Unchanged: Docs (6710).

packages/site
Category main PR Δ
Total 24564 24564 0

Unchanged: src/demos/** (14189), src/components/** (3032), src/lib/** (2688), src/stores/** (26), Other (4629).

packages/demos
Category main PR Δ
Total 3064 3064 0

Unchanged: src/hn/** (1072), Other (1992).

packages/minimal
Category main PR Δ
Total 550 550 0

Unchanged: Other (550).

packages/minimal-rsvelte
Category main PR Δ
Total 548 548 0

Unchanged: Other (548).

packages/support
Category main PR Δ
Total 3301 3301 0

Unchanged: src/**/*.test.ts (630), src/admin/** (671), src/components/** (84), src/*.server.ts (379), src/{index,routes}.ts (237), Other (1300).

packages/shared
Category main PR Δ
Total 44 44 0

Unchanged: Other (44).

packages/mochi-rsvelte
Category main PR Δ
Total 227 227 0

Unchanged: Other (227).

packages/mochi-svelte-shaker
Category main PR Δ
Total 184 184 0

Unchanged: Other (184).

packages/cli
Category main PR Δ
Total 1066 1066 0

Unchanged: src/**/*.test.ts (440), src/cli* (165), src/{create,templates,utils}.ts (277), Other (184).

packages/video-animations
Category main PR Δ
Total 646 646 0

Unchanged: src/{frame,anim,theme}.ts (389), src/{render,render-worker,generate}.ts (182), Other (75).

packages/msgpackr-extract-stub
Category main PR Δ
Total 29 29 0

Unchanged: *.test.ts (22), Other (7).

The font/asset tests pulled url() and format() values out of Bun's
printed CSS with regexes, so they depended on Bun's quoting. Read them
via parseCss, as production does, which decodes quoting.
@khromov
khromov merged commit 633fdd0 into main Oct 30, 2026
9 checks passed
@khromov
khromov deleted the fix/bun-canary-css-assets branch October 30, 2026 01:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant